package com.leo.boot.feature.merge;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Stream;

import static com.leo.boot.feature.merge.Merging.Scope.SUPPORTED;
import static com.leo.boot.feature.merge.MergingDelegate.MethodType.*;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

@Slf4j
public class MergingDelegate {

    Field field;

    Class<?> clazz;

    Merging fieldMerging;

    Merging clazzMerging;

    Map<MethodType, Method> methods = new EnumMap<>(MethodType.class);

    Map<MethodType, Class<?>> utils = new EnumMap<>(MethodType.class);

    MergingDelegate(Field field) {
        this.field = field;
        this.clazz = field.getDeclaringClass();
        this.fieldMerging = requireNonNull(findAnnotation(field));
        this.clazzMerging = requireNonNull(findAnnotation(clazz));

        String name = field.getName();
        Class<?> type = field.getType();

        initMethod(get, asList(get + capitalize(name), name), clazz);
        initMethod(set, asList(set + capitalize(name), name), clazz, type);
        initMethod(op, asList(fieldMerging.op()), type, type);
        initMethod(test, List.of(fieldMerging.test()), type, type);
    }

    MergingDelegate(Class<?> clazz) {
        this.clazz = clazz;
        this.clazzMerging = requireNonNull(findAnnotation(clazz));

        initMethod(pre, List.of(clazzMerging.pre()), clazz);
        initMethod(post, List.of(clazzMerging.post()), clazz);
    }

    public static MergingDelegate initClazzDelegate(Class<?> clazz) {
        return new MergingDelegate(clazz);
    }

    public static List<MergingDelegate> initFieldDelegates(Class<?> clazz) {
        Merging clazzMerging = requireNonNull(findAnnotation(clazz));
        return Stream.of(clazz.getDeclaredFields()).filter(field -> {
            if (field.isAnnotationPresent(Merging.class)) {
                return !field.getDeclaredAnnotation(Merging.class).ignore();
            }
            if (clazzMerging.scope() == SUPPORTED) {
                return Arrays.stream(clazzMerging.supported()).anyMatch(type -> type.isAssignableFrom(field.getType()));
            }
            return false;
        }).map(MergingDelegate::new).collect(toList());
    }

    private static Merging findAnnotation(Field field) {
        if (field.isAnnotationPresent(Merging.class)) {
            return field.getAnnotation(Merging.class);
        }
        return findAnnotation(field.getDeclaringClass());
    }

    private static Merging findAnnotation(Class<?> clazz) {
        if (clazz == null || clazz == Object.class) {
            return null;
        }
        if (clazz.isAnnotationPresent(Merging.class)) {
            return clazz.getAnnotation(Merging.class);
        }
        Merging annotation = findAnnotation(clazz.getSuperclass());
        if (annotation != null) {
            return annotation;
        }
        for (Class<?> face : clazz.getInterfaces()) {
            Merging merging = findAnnotation(face);
            if (merging != null) {
                return merging;
            }
        }
        return null;
    }

    private static Method findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) {
        try {
            return clazz.getDeclaredMethod(methodName, paramTypes);
        } catch (NoSuchMethodException | SecurityException e) {
            try {
                Class<?>[] objectTypes = new Class[paramTypes.length];
                Arrays.fill(objectTypes, Object.class);
                return clazz.getDeclaredMethod(methodName, objectTypes);
            } catch (NoSuchMethodException | SecurityException ex) {
                return null;
            }
        }
    }

    private static String capitalize(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }

    public <T extends Mergeable<T>> Object get(T a) {
        return invokeMethod(get, a);
    }

    public <T extends Mergeable<T>> void set(T a, Object value) {
        invokeMethod(set, a, value);
    }

    public Object operate(Object a, Object b) {
        return invokeMethod(op, a, b);
    }

    @SneakyThrows
    public Object nil() {
        if (!fieldMerging.nil().isEmpty()) {
            return field.getType().getConstructor(String.class).newInstance(fieldMerging.nil());
        }
        return null;
    }

    public boolean condition(Object a, Object b) {
        if (methods.containsKey(test)) {
            Object result = invokeMethod(test, a, b);
            return result != null && (boolean) result;
        }
        return true;
    }

    public void pre(Object a) {
        if (methods.containsKey(pre)) {
            invokeMethod(pre, a);
        }
    }

    public void post(Object a) {
        if (methods.containsKey(post)) {
            invokeMethod(post, a);
        }
    }

    @SneakyThrows
    private void initMethod(MethodType key, List<String> methodNames, Class<?> clazz, Class<?>... paramTypes) {
        if (methodNames.stream().allMatch(String::isEmpty)) {
            return;
        }

        for (String methodName : methodNames) {
            Method method = findMethod(clazz, methodName, paramTypes);
            if (method != null) {
                methods.put(key, method);
                log.debug("{} - {} implementation is {}.", getTarget(), key, method);
                return;
            }
        }

        paramTypes = Stream.concat(Stream.of(clazz), Stream.of(paramTypes)).toArray(Class<?>[]::new);

        Set<Class<?>> utilities = Stream.of(fieldMerging, clazzMerging).map(Merging::utils).flatMap(Stream::of).collect(toSet());
        for (String methodName : methodNames) {
            for (Class<?> utility : utilities) {
                Method method = findMethod(utility, methodName, paramTypes);
                if (method != null) {
                    methods.put(key, method);
                    utils.put(key, utility);
                    log.debug("{} - {} implementation is {}.", getTarget(), key, method);
                    return;
                }
            }
        }

        throw new NoSuchMethodException("Any of " + methodNames + " not found in " + clazz + " or " + utilities);
    }

    @SneakyThrows
    private Object invokeMethod(MethodType key, Object obj, Object... params) {
        if (!utils.containsKey(key)) {
            return methods.get(key).invoke(obj, params);
        } else {
            params = Stream.concat(Stream.of(obj), Stream.of(params)).toArray(Object[]::new);
            return methods.get(key).invoke(utils.get(key), params);
        }
    }

    private Object getTarget() {
        return field == null ? clazz : field;
    }

    enum MethodType {
        get, set, op, test, pre, post
    }
}
